Le carnet On est
Annale 2024-2025 · Examen Général (Rattrapage) · ING2-GIA

Examen Réseau — Examen Général

2 heures, 4 exercices : procédure d'écriture sur fichier et pipeline père/fils avec remplacement de caractères, threads en parallèle puis en séquence, problème du coiffeur dormant (sémaphores + threads), serveur de fichiers TCP multi-clients.

2h · ING2-GIA 4 exercices · 20 points 1 feuille recto-verso autorisée

i. Le sujet

Le PDF ne s'affiche pas. Télécharger le sujet.

↗ Sujet en plein écran

ii. Notions mobilisées

ExoSujetNotion cléChapitre
1Ecrire_fich + pipe entre filsopen avec droits, lecture stdin, fork + pipeCours 1 · Cours 3
2Threads 1-50 et 51-100pthread_create, pthread_join, synchronisation par mutexCours 4
3Coiffeur dormantSémaphores + threads, sleep/wake-upCours 2 · Cours 4
4Serveur de fichiersSocket TCP, read fichier, fork multi-clientsCours 5

1. Exercice 1 — Écriture sur fichier et pipeline (6 pts)

Q1 — Procédure Ecrire_fich

Crée un fichier avec droits rw pour user et groupe (mode 0664), lit stdin et écrit jusqu'à recevoir la chaîne « stop ».

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/types.h>
#include <sys/stat.h>

void Ecrire_fich(const char* chemin_du_fichier) {
   // O_CREAT pour créer, mode 0664 = rw-rw-r--
   int fd = open(chemin_du_fichier,
                 O_WRONLY | O_CREAT | O_TRUNC,
                 S_IRUSR | S_IWUSR | S_IRGRP | S_IWGRP);
   if (fd < 0) {
      perror("open");
      return;
   }

   char buf[256];
   while (1) {
      printf("> ");
      if (scanf("%255s", buf) != 1) break;
      if (strcmp(buf, "stop") == 0) break;
      write(fd, buf, strlen(buf));
      write(fd, " ", 1);                  // séparateur
   }
   close(fd);
}

Q2 — Procédure remplace_Q_to_A

void remplace_Q_to_A(char tab[], int taille) {
   for (int i = 0; i < taille; i++) {
      if (tab[i] == 'q') tab[i] = 'a';
   }
}

Q3 — Pipeline père / 2 fils

Le père crée deux fils. Fils 1 lit fichier1 et écrit sur un pipe. Fils 2 lit le pipe, remplace q par a et écrit dans fichier2.

int main(int argc, char** argv) {
   if (argc != 3) {
      fprintf(stderr, "Usage : %s <fichier1> <fichier2>\n", argv[0]);
      exit(1);
   }

   int p[2];
   pipe(p);

   pid_t f1 = fork();
   if (f1 == 0) {                        // ─── FILS 1 : lecture fichier1 → pipe ───
      close(p[0]);                       // pas besoin de lire
      int fd = open(argv[1], O_RDONLY);
      char buf[128];
      int n;
      while ((n = read(fd, buf, 128)) > 0) {
         write(p[1], buf, n);
      }
      close(fd);
      close(p[1]);                       // crucial : signale EOF au fils 2
      exit(0);
   }

   pid_t f2 = fork();
   if (f2 == 0) {                        // ─── FILS 2 : pipe → remplace → fichier2 ───
      close(p[1]);                       // pas besoin d'écrire
      int fd = open(argv[2], O_WRONLY | O_CREAT | O_TRUNC, 0664);
      char buf[128];
      int n;
      while ((n = read(p[0], buf, 128)) > 0) {
         remplace_Q_to_A(buf, n);
         write(fd, buf, n);
      }
      close(fd);
      close(p[0]);
      exit(0);
   }

   // ─── PÈRE ───
   close(p[0]);                          // le père n'utilise pas le pipe
   close(p[1]);
   wait(NULL); wait(NULL);              // attend les deux fils
   return 0;
}
⚠ Le piège des descripteurs Le père ouvre le pipe puis le passe aux deux fils par héritage. Le père doit fermer ses copies de p[0] et p[1], sinon le fils 2 ne détectera jamais EOF (puisque le père garde un descripteur écriture ouvert). Idem pour les fils : ils ferment l'extrémité inutile.

2. Exercice 2 — Threads (4 pts)

Q1 — Deux threads en parallèle

#include <stdio.h>
#include <pthread.h>

void* afficher_intervalle(void* arg) {
   int* bornes = (int*)arg;
   for (int i = bornes[0]; i <= bornes[1]; i++) {
      printf("%d\n", i);
   }
   return NULL;
}

int main(void) {
   pthread_t t1, t2;
   int args1[2] = {1, 50};
   int args2[2] = {51, 100};

   pthread_create(&t1, NULL, afficher_intervalle, args1);
   pthread_create(&t2, NULL, afficher_intervalle, args2);

   pthread_join(t1, NULL);
   pthread_join(t2, NULL);
   return 0;
}

Sortie observée : les nombres apparaissent entremêlés (1, 51, 2, 52, … ou autre ordre selon l'ordonnanceur).

Q2 — Forcer l'ordre 1, 2, 3, …, 99, 100

Pour garantir t1 termine avant que t2 n'affiche, deux approches.

Solution simple — join séquentiel Créer t1, pthread_join(t1), puis créer t2.
pthread_create(&t1, ..., args1);
pthread_join(t1, NULL);
pthread_create(&t2, ..., args2);
pthread_join(t2, NULL);
Solution synchronisée — sémaphore Créer les deux d'un coup mais bloquer t2 en attente d'un sémaphore que t1 libère à la fin.
// t1 finit : sem_post(&sync)
// t2 commence par : sem_wait(&sync)

La solution simple est suffisante ici. Le sémaphore devient utile quand on veut une interaction plus fine (par exemple alterner deux threads à chaque pas).

3. Exercice 3 — Le coiffeur dormant (4 pts)

Problème classique de synchronisation Un coiffeur dort si aucun client. Un client arrivant le réveille et s'assoit pour sa coupe. Si le coiffeur est occupé, le client s'endort dans la salle d'attente (NBP places). Si la salle est pleine, il repart. Quand la coupe finit, le coiffeur réveille un client de la salle d'attente (ou se rendort).

Les sémaphores nécessaires

SémaphoreInitRôle
client_arrive0Le coiffeur attend (P) qu'un client le réveille (V). Sémaphore de signal.
coupe_finie0Le client attend (P) que le coiffeur termine sa coupe (V).
mutex1Protège le compteur nb_attente.

Code

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <pthread.h>
#include <semaphore.h>

#define NBP 3                                // nombre de places en salle d'attente

sem_t client_arrive, coupe_finie;
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
int nb_attente = 0;

void* coiffeur(void* arg) {
   while (1) {
      sem_wait(&client_arrive);              // dort jusqu'à un client

      pthread_mutex_lock(&mutex);
      nb_attente--;                          // un client de moins dans la salle
      pthread_mutex_unlock(&mutex);

      printf("Coiffeur : coupe en cours...\n");
      sleep(2);                              // simule la coupe
      printf("Coiffeur : coupe terminée\n");

      sem_post(&coupe_finie);                // libère le client
   }
   return NULL;
}

void* client(void* arg) {
   int id = *(int*)arg;

   pthread_mutex_lock(&mutex);
   if (nb_attente < NBP) {
      nb_attente++;
      printf("Client %d : entre, salle = %d/%d\n", id, nb_attente, NBP);
      pthread_mutex_unlock(&mutex);

      sem_post(&client_arrive);              // réveille le coiffeur
      sem_wait(&coupe_finie);                // attend sa coupe
      printf("Client %d : coiffé, je pars\n", id);
   } else {
      pthread_mutex_unlock(&mutex);
      printf("Client %d : salle pleine, je repars\n", id);
   }
   return NULL;
}

int main(void) {
   sem_init(&client_arrive, 0, 0);
   sem_init(&coupe_finie,   0, 0);

   pthread_t tcoif;
   pthread_create(&tcoif, NULL, coiffeur, NULL);

   pthread_t clients[10];
   int ids[10];
   for (int i = 0; i < 10; i++) {
      ids[i] = i + 1;
      pthread_create(&clients[i], NULL, client, &ids[i]);
      usleep(500000);                       // 0.5s entre arrivées
   }

   for (int i = 0; i < 10; i++) pthread_join(clients[i], NULL);
   return 0;
}
🔑 Pourquoi un mutex pour nb_attente ?
Deux clients peuvent arriver simultanément et lire nb_attente en même temps. Sans mutex, ils pourraient tous deux décider qu'il reste une place et faire nb_attente++, créant un dépassement. Le mutex linéarise les accès.

Compilation : gcc coiffeur.c -lpthread -o coiffeur.

4. Exercice 4 — Serveur de fichiers TCP (6 pts)

Client envoie un nom de fichier. Serveur cherche dans son répertoire Service/ et renvoie soit le contenu, soit un message d'erreur.

Q1 — Client TCP

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>

int main(int argc, char** argv) {
   if (argc != 4) {
      fprintf(stderr, "Usage : %s <ip_serveur> <port> <nom_fichier>\n", argv[0]);
      exit(1);
   }

   int sock = socket(AF_INET, SOCK_STREAM, 0);
   struct sockaddr_in serveur;
   serveur.sin_family = AF_INET;
   serveur.sin_port   = htons(atoi(argv[2]));
   inet_aton(argv[1], &serveur.sin_addr);

   if (connect(sock, (struct sockaddr*)&serveur, sizeof(serveur)) < 0) {
      perror("connect"); exit(1);
   }

   // Envoi du nom de fichier
   write(sock, argv[3], strlen(argv[3]));

   // Réception du contenu (ou message d'erreur)
   char buf[1024];
   int n;
   while ((n = read(sock, buf, sizeof(buf) - 1)) > 0) {
      buf[n] = '\0';
      if (strstr(buf, "fichier introuvable") != NULL) {
         printf("le fichier n'existe pas chez le serveur\n");
      } else {
         printf("%s", buf);             // affiche le contenu
      }
   }
   close(sock);
   return 0;
}

Q2 — Serveur (un seul client puis multi-clients)

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <fcntl.h>
#include <signal.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <sys/stat.h>
#include <netinet/in.h>

void traiter_client(int dialogue) {
   char nom[128];
   int n = read(dialogue, nom, 127);
   if (n <= 0) { close(dialogue); return; }
   nom[n] = '\0';

   char chemin[256];
   snprintf(chemin, sizeof(chemin), "Service/%s", nom);

   int fd = open(chemin, O_RDONLY);
   if (fd < 0) {
      char* msg = "fichier introuvable\n";
      write(dialogue, msg, strlen(msg));
   } else {
      char buf[1024];
      int m;
      while ((m = read(fd, buf, sizeof(buf))) > 0) {
         write(dialogue, buf, m);
      }
      close(fd);
   }
   close(dialogue);
}

int main(int argc, char** argv) {
   if (argc != 2) {
      fprintf(stderr, "Usage : %s <port>\n", argv[0]);
      exit(1);
   }

   signal(SIGCHLD, SIG_IGN);                     // évite les zombies

   int ecoute = socket(AF_INET, SOCK_STREAM, 0);
   struct sockaddr_in serveur = {0}, client;
   socklen_t len = sizeof(client);

   serveur.sin_family      = AF_INET;
   serveur.sin_port        = htons(atoi(argv[1]));
   serveur.sin_addr.s_addr = INADDR_ANY;

   bind(ecoute, (struct sockaddr*)&serveur, sizeof(serveur));
   listen(ecoute, 10);
   printf("Serveur fichiers en écoute sur %s...\n", argv[1]);

   while (1) {
      int dialogue = accept(ecoute, (struct sockaddr*)&client, &len);
      if (dialogue < 0) continue;

      if (fork() == 0) {                       // enfant : traite le client
         close(ecoute);
         traiter_client(dialogue);
         exit(0);
      } else {                                // parent : retourne accepter
         close(dialogue);
      }
   }
   return 0;
}
Version mono-client (Q4.1) Pour la version simple sans fork, on enlève le bloc fork() et on appelle directement traiter_client(dialogue);. Le serveur traite alors un seul client à la fois — les autres clients attendent dans la file de connexions (listen).

Q2 modification multi-clients — alternatives à fork

  • pthread_create par client : plus léger qu'un fork, mais demande de gérer les mutex si les threads partagent des ressources.
  • select ou epoll : serveur mono-thread qui multiplexe les I/O. Le plus performant à grande échelle (modèle réacteur).
  • Pool de threads : pré-créer N threads qui pickent dans une file de connexions. Évite le coût de création à chaque client.

Notions ré-activées

  • open avec O_CREAT demande un mode (0644, 0664, etc.)
  • scanf("%s") s'arrête au premier espace
  • strcmp = 0 pour égalité — toujours le piège oublié
  • Pipeline 2 fils : père crée le pipe, ferme ses copies, attend les deux fils
  • Threads séquentiels : pthread_join avant le pthread_create suivant
  • Coiffeur dormant : 2 sémaphores signal (client_arrive, coupe_finie) + 1 mutex (nb_attente)
  • Mutex obligatoire autour de tout compteur partagé entre threads
  • signal(SIGCHLD, SIG_IGN) évite les zombies dans le serveur fork
  • snprintf pour construire des chemins en sécurité (limite la taille)
  • Multi-clients : fork (simple), threads (rapide), select (scalable)